#!/usr/bin/env python
"""
Copyright (C) 2009 pk

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
"""

import xmlrpclib
import time
from optparse import OptionParser
import logging, logging.handlers
import re
import sys
import socket

LOG_FILENAME = "logs/portage-client.log"

parser = OptionParser()
logger = logging.getLogger("portage-client")

# def setup_logger():
#     logger.setLevel(logging.DEBUG)


def setup_logger():

    logger.setLevel(logging.DEBUG)
    fh = logging.FileHandler("logs/portage-client.log")
    fh.setLevel(logging.DEBUG)
    # create console handler with a higher log level
    ch = logging.StreamHandler(sys.stdout,)
    ch.setLevel(logging.INFO)
    # create formatter and add it to the handlers
    formatter = logging.Formatter(
        "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    fh.setFormatter(formatter)
    ch.setFormatter(formatter)
    # add the handlers to the logger
    logger.addHandler(fh)
    logger.addHandler(ch)


setup_logger()


def add_options():
    choices = ['all', 'make.conf', 'package.use', 'package.mask', 'package.unmask',
               'package.keywords', 'package.keywords', 'profile']
    parser.add_option("-p", "--pretend", action="store_true", default=False,
                      dest="pretend",
                      help="Pretend operation")
    parser.add_option("-s", "--search",
                      action="store_true", dest="search", default=False,
                      help="Port to bind to")
    parser.add_option("--push", dest="push",
                      help = "Push files. Choices are: %s " %(choices))
    parser.add_option("-u", "--update",
                      dest="update", action="store_true", default=False,
                      help = "Update package or type")
    parser.add_option("-a", "--add",
                      dest="add",
                      help = "Add type to database. Must have write \
                      permission to database")
    parser.add_option("-t", "--type",
                      dest="type", default=None,
                      help = "What type of server to perform operations on.\n\
                      See --list-types for listing server types")
    parser.add_option("-i", "--install",
                      dest = "install", default = False, action = "store_true",
                      help = "Install package")
    parser.add_option("--list-types", dest = "list_types", default = False,
                     action = "store_true", help = "List server types")
    parser.add_option("--master-exit", dest = "master_exit", action = "store_true",
                     default = False, help = "Sends signal to portage-master service to shutdown.")
    parser.add_option("--sync", dest = "sync", action = "store_true",
                     default = False, help = "Syncs all machines of type with portage-master.")
    parser.add_option("--exit", dest = "server_exit", action = "store_true",
                     default = False, help = "Sends signal to service on machines of type to shutdown.")
    parser.add_option("-C", "--remove", dest = "remove", action = "store_true",
                     default = False, help = "Remove package(s) from machines of type")
    # parser.add_option()

add_options()


class PortageClient(object):
    _portage_master = "portage-master"
    _portage_master_port = 1590
    _alpha_rec = re.compile('\w+')
    _push_types = ['all', 'make.conf', 'package.use',
                   'package.mask', 'package.unmask',
                   'package.keywords', 'profile']

    def log_output(self, output):
        try:
            for line in output:
                logger.info("%s: %s" %(line, output[line]))
        except TypeError:
            logger.info(output)


    def __init__(self, portage_master = None, portage_master_port = None,
                 options = None, args = None):
        if portage_master:
            self._portage_master = portage_master
        if portage_master_port:
            self._portage_master_port = portage_master_port


        self._init_master()


    def _check_push_types(self, choice, server_type):
        self._push_choices = { self._push_types[0] : self.portage_files_push_all,
                               self._push_types[1] : self.portage_files_push_make,
                               self._push_types[2] : self.portage_files_push_package_use,
                               self._push_types[3] : self.portage_files_push_package_mask,
                               self._push_types[4] : self.portage_files_push_package_unmask,
                               self._push_types[5] : self.portage_files_push_package_keywords,
                               self._push_types[6] : self.portage_files_push_profile }

        if choice in self._push_choices:
            self._push_choices[choice](server_type)
            return True
        else:
            logger.error("%s is not a valid option." % (choice))
            return None


    def echo(self):
        print "Blah"


    def install_packages(self, packages, type):
        if not self.check_packages(packages):
            return None
        for package in packages:
            self.install_package(package, type)


    def pretend_packages(self, packages, type):
        if not self.check_packages(packages):
            return None
        for package in packages:
            self.pretend_package(package, type)


    def update_packages(self, packages, type):
        if not self.check_packages(packages):
            return None
        for package in packages:
            self.update_package(package, type)


    def search_packages(self, packages, type):
        if not self.check_packages(packages):
            return None
        for package in packages:
            self.search_package(package, type)

        
    def update_pretend_packages(self, packages, type):
        if not self.check_packages(packages):
            return None
        for package in packages:
            self.update_pretend(package, type)


    def remove_packages(self, packages, type):
        if not self.check_packages(packages):
            return None
        for package in packages:
            self.remove_package(package, type)


    def check_packages(self, packages):
        if not packages:
            return None
        checks = False
        for package in packages:
            if self._alpha_rec.match(package):
                checks = True
        return checks

    
    def _init_master(self):
        self.master = xmlrpclib.ServerProxy("http://%s:%d" %(self._portage_master,
                                                             self._portage_master_port),
                                            verbose=False)
        logger.debug("%s" % (self._system_list_methods()))
        

    def _system_list_methods(self):
        output = self._try_xml_rpc(self.master.system.listMethods)
        self.log_output(output)
        return output
                
    def install_package(self, package, type):
        # output = s.portage_install_type_package('vim', 'prod')
        output = self._try_xml_rpc(self.master.portage_install_type_package,
                                   package, type)
        self.log_output(output)
        return output


    def pretend_package(self, package, type):
        # output = s.portage_install_type_package('vim', 'prod')
        output = self._try_xml_rpc(self.master.portage_pretend_type_package,
                                   package, type)
        self.log_output(output)
        return output


    def search_package(self, package, type):
        output = self._try_xml_rpc(self.master.portage_search_type_package,
                                   package, type)
        self.log_output(output)
        return output


    def get_latest_version(self, package, type):
        output = self._try_xml_rpc(self.master.portage_latest_version_type_package,
                                   package, type)
        self.log_output(output)
        return output


    def update_system(self, type):
        output = self._try_xml_rpc(self.master.portage_update_type_system, type)
        self.log_output(output)
        return output


    def update_package(self, package, type):
        output = self._try_xml_rpc(self.master.portage_update_type_package, package, type)
        self.log_output(output)
        return output


    def remove_package(self, package, type):
        output = self._try_xml_rpc(self.master.portage_remove_type_package, package, type)
        self.log_output(output)
        return output


    def update_pretend(self, package, type):
        output = self._try_xml_rpc(self.master.portage_update_pretend_type_package, package, type)
        self.log_output(output)
        return output


    def portage_sync(self, type):
        output = self._try_xml_rpc(self.master.portage_sync_type, type)
        self.log_output(output)
        return output


    def master_exit(self):
        output = self._try_xml_rpc(self.master.service_stop)
        self.log_output(output)
        return output


    def portage_server_exit(self, type):
        output = self._try_xml_rpc(self.master.servers_stop_type, type)
        self.log_output(output)
        return output


    def ebuild_package(self, package):
        output = self._try_xml_rpc(self.master.portage_ebuild_package,
                                   package)
        self.log_output(output)
        return output
        

    def db_add_type(self, type):
        output = self._try_xml_rpc(self.master.db_add_type, type)
        self.log_output(output)
        return output

    def db_get_types(self):
        output = self._try_xml_rpc(self.master.db_get_types)
        self.log_output(output)
        return output


    def push_files(self, file_to_push, type):
        return self._check_push_types(file_to_push, type)
        

    def push_all_files(self, type):
        output = self._try_xml_rpc(self.master.portage_push_files_type, type)
        self.log_output(output)
        return output

    def list_types(self):
        return self.db_get_types()
        

    def _try_xml_rpc(self, func, *args):
        try:
            output = func(*args)
        except socket.error, (code, msg):
            if code == 111 and msg == 'Connection refused':
                logger.fatal("Portage master (%s:%d) is not listening."%(self._portage_master,
                                                                         self._portage_master_port))
                sys.exit(1)
            else:
                logger.fatal(" - Error code: %d - Error Status: %s" %(code, msg))
                sys.exit(1)
        except socket.gaierror, (code, msg):
            if code == -2:
                logger.fatal("Unable to lookup hostname %s." %(self._portage_master))
                sys.exit(1)
        return output

    def portage_files_push_all(self, type):
        return self.push_all_files(type)

    def portage_files_push_make(self):
        pass

    def portage_files_push_package_use(self):
        pass

    def portage_files_push_package_mask(self):
        pass

    def portage_files_push_package_unmask(self):
        pass

    def portage_files_push_package_keywords(self):
        pass

    def portage_files_push_profile(self):
        pass

def test():
    client = PortageClient()
    print client.push_all_files('prod')
    # print client._check_push_types('all')


    
    ###  Tested Working
    # client.pretend_packages(['world'], 'prod')
    # client.update_system('prod')
    # client.search_package('gcc', 'prod')
    
    


    ### Tested not working
    # client.get_latest_version(['gcc'], 'prod')
    # client.push_all_files('prod')
    # client.install_packages(['gcc'], 'prod')
    
    
#         self.pretend = pretend
#         self.update = update
#         self.search = search
#         self.install = install
#         self.push = push
#         self.package = packages
#         have_run = False
#         if (self.pretend or self.update or self.search or self.install) and (self.packages):
#             self.check_packages(packages)
#             have_run = True

#         if self.push:
#             self._check_push_types(self.push)
#             have_run = True

#         if not have_run:
#             logger.error("No command given")
#             sys.exit(1)

def main():
    (options, args) = parser.parse_args()
    client = PortageClient()
    parse_options(options, args, client)
    # client._system_list_methods()
    # test_master()
    

def server_type_error():
    logger.fatal("Server type to run command is required.\
    See --list-types for listing server types")
    sys.exit(1)

def check_type(type):
    if (type):
        if PortageClient._alpha_rec.match(type):
            return type
        else:
            logger.info("Not a valid server type - %s" % (type))
            

def parse_options(options, args, client):
    did_action = False

    if options.master_exit:
        if client.master_exit():
            logger.info("Portage-master service shutting down")
        else:
            logger.error("Portage-master service could not be shut down")
        sys.exit(0)

    if options.server_exit:
        type = check_type(options.type)
        if type:
            client.portage_server_exit(type)
        else:
            server_type_error()
        sys.exit(0)
    if (options.push):
        did_action = True
        if options.push in PortageClient._push_types:
            type = check_type(options.type)
            if type:
                client._check_push_types(options.push, options.type)
            else:
                server_type_error()
        else:
            logger.error("%s is not a valid option." % (options.push))
            parser.print_help()
    if (options.add):
        did_action = True
        if PortageClient._alpha_rec.match(options.add):
            logger.info("Adding %s as database type" %(options.add))
            client.db_add_type(options.add)
    if (options.list_types):
        did_action = True
        logger.info("Server types:")
        for type in client.list_types():
            logger.info("%s" % (type))

    if (options.pretend):
        did_action = True
        type = check_type(options.type)
        if type:
            if args:
                if options.update:
                    client.update_pretend_packages(args, type)
                else:
                    client.pretend_packages(args, type)
            else:
                logger.error("Packages required")
        else:
            server_type_error()
        sys.exit(0)
    if options.remove:
        did_action = True
        type = check_type(options.type)
        if type:
            if args:
                client.remove_packages(args, type)
            else:
                logger.error("Packages required")
        else:
            server_type_error()

    if (options.search):
        did_action = True
        type = check_type(options.type)
        if type:
            if args:
                client.search_packages(args, type)
            else:
                logger.error("Packages required")
                parser.print_help()
        else:
            server_type_error()
    if (options.install):
        did_action = True
        type = check_type(options.type)
        if type:
            if args:
                client.install_packages(args, type)
            else:
                logger.error("Packages required")
                parser.print_help()
        else:
            server_type_error()
    if options.update:
        did_action = True
        type = check_type(options.type)
        if type:
            if args:
                client.update_packages(args, type)
            else:
                logger.error("Packages required")
                parser.print_help()
        sys.exit(0)
    if (options.sync):
        did_action = True
        type = check_type(options.type)
        if type:
            client.portage_sync(type)
        else:
            server_type_error()
    if not did_action:
        parser.print_help()
        


def print_output(output):
    try:
        for line in output:
            print "%s: %s" %(line, output[line])
    except TypeError:
        print output

def test_master_multicall(s):
    ips = s.get_ips_type('prod')
    multicall = xmlrpclib.MultiCall(s)
    multicall.portage_pretend('gcc', ips[0])
    multicall.portage_pretend('gcc', ips[1])
    start = time.time()
    result = multicall()
    print "%s, %s" %tuple(result)
    print "Elapsed Time: %s" % (time.time() - start)
    


def portage_push_files(s):
    data = {}
    for filename in ['make.conf', 'package.keywords', 'package.use', 'package.mask', 'package.unmask']:
        try:
            file = open(filename)
        except IOError, e:
            print e
        bin = xmlrpclib.Binary(file.read())
        data[filename] = bin.data
    print data
    return s.portage_push_files(data)


def test_server(ip):
    s = xmlrpclib.ServerProxy('http://' + ip + ':1591')
    
    # print s.pow(2,3)  # Returns 2**3 = 8
    # print s.add(2,3)  # Returns 5
    # print s.someFunc()
    # print s.div(5,2)  # Returns 5//2 = 2
    # print s.getDf()
    # print s.uname()
    print s.system.listMethods()
    # print s.portage_pretend('gcc')
    # print s.portage_update_system()
    # print s.some_text('dfadfa')
    # print s.make_package_fullname('gcc')
    # print s.portage_add_keyword_package('gcc')
    print portage_push_files(s)
#     try:
#         file = open('package.unmask')
#     except IOError, e:
#         print e
#     bin = xmlrpclib.Binary(file.read())
#     print s.push_package_unmask_file(bin.data)

    # print 

    # print s.ebuild_pkg('vim')


# test_server('192.168.0.101')
# test_master()


# print s.add(31231,123)
# print s.get_latest_version('gcc')
# print s.make_package_dir('sys-devel/gcc-4.3.2-r4')
# print s.portage_verbose('gcc', ['-p'])
# print s.ebuild_pkg('eselect')
# print s.is_package_installed('gcc')
# print s.portage_install('eselect')
# print s.return_arr()
# try:
# print s.portage_pretend('system')
# print s.portage_sync()
# except:
#     pass


# print returnstr


# except xml.parsers.expat.ExpatError:
#     pass


# print s.portage_pretend_world()
# print out


# Print list of available methods
# for method in s.system.listMethods():
#     print method
#     print s.system.methodHelp(method)
    
if __name__ == '__main__':
    main()
    # test()
